package testrank.affinity;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import edu.smu.tspell.wordnet.NounSynset;
import edu.smu.tspell.wordnet.Synset;
import edu.smu.tspell.wordnet.SynsetType;
import edu.smu.tspell.wordnet.WordNetDatabase;
import edu.smu.tspell.wordnet.impl.file.Morphology;

/**
 * @author Hagai Cibulski
 */
public class WordNetConnector {
	
	public static final int MAX_DISTANCE = 15;
	public static final int MAX_HYPONYM_DISTANCE = 9;
	public static final int MAX_PART_DISTANCE = 3;
	public static final int DISTANCE_NOT_FOUND = (int)(1.2 * MAX_DISTANCE);
	public static final int MAX_HEIGHT = 10;//15?
	private static final WordNetDatabase database = WordNetDatabase.getFileInstance();

	
	public int wordDistance(String w1, String w2, Set<String> g1, Set<String> g2) {
		return wordDistanceAsy(w1, w2, g1, g2);
//		if (equivalentWords(w1, w2)) {
//			return 1;
//		}
//		return Math.min(wordDistanceAsy(w1, w2), wordDistanceAsy(w2, w1));
	}

	private int wordDistanceAsy(String w1, String w2, Set<String> g1, Set<String> g2) {
		int d = wordDistanceLCS(w1, w2, g1, g2);
		return d;
	}

	class Heights {
		int[] heights = new int[] {MAX_HEIGHT, MAX_HEIGHT};
	}
	
	/**
	 * Least Common Subsumer (Ancestor) Algorithm:
	 * <pre>
	 *  wordDistance(w1, w2)
	 * 		if w1 ~ w2 then return 1
	 *
	 * 		Q1 = synsets of w1
	 * 		Q2 = synsets of w2
	 * 		height = 1
	 * 		upper = infinity
	 * 		while height < upper
	 * 		  for i=1,2
	 * 			Q` = {}
	 * 			for each s in Qi
	 * 				add s.getHypernyms to Q` //TODO use handledSynsets to avoid doing this twice? 
	 * 				h = m.get(s)
	 * 				if no h
	 * 					new h; h[i]=height
	 * 					m.put(s, h)
	 * 				else
	 * 					h[i] = min{h[i], height}
	 * 					if h[~i] < infinity 
	 *   					upper = min{upper, h[i] + h[~i])
	 *   		Qi = Q`  			
	 * 		  height++
	 * 		return upper			
	 * </pre>
	 */
	public int wordDistanceLCS(String w0, String w1, Set<String> g0, Set<String> g1) {
		if (equivalentWords(w0, w1)) {
			return 1;
		}
		return Math.min(wordDistanceLCS1(w0, w1, g0, g1), wordDistanceLCS1(w1, w0, g1, g0));
	}

	public int wordDistanceLCS1(String w0, String w1, Set<String> g0, Set<String> g1) {
		List<Set<Synset>> q = new ArrayList<Set<Synset>>();
		q.add(new HashSet<Synset>());
		q.add(new HashSet<Synset>());

		q.get(0).addAll(getSynsets(w0, g0));
		q.get(1).addAll(getSynsets(w1, g1));
		
		return synsetDistanceLCS(q);
	}


	private int synsetDistanceLCS(List<Set<Synset>> q) {
		Map<Synset, Heights> m = new HashMap<Synset, Heights>(); 
		int height = 1;
		int upper = DISTANCE_NOT_FOUND;
		
		while (height < upper &&
				(!q.get(0).isEmpty() || !q.get(1).isEmpty())) {
			for (int i = 0; i < 2; i++) {
				Set<Synset> qi = q.get(i);
				Set<Synset> hypernyms = new HashSet<Synset>();
				for (Synset s : qi) {
						
					if (s.getType() == SynsetType.NOUN) {
						NounSynset noun = (NounSynset)s;
						hypernyms.addAll(Arrays.asList(noun.getHypernyms()));
					}
					
					Heights h = m.get(s);
					if (h == null) {
						h = new Heights();
						h.heights[i] = height;
						m.put(s, h);
					}
					else {
						h.heights[i] = Math.min(h.heights[i], height);
						if (h.heights[1-i] < MAX_HEIGHT) { 
							upper = Math.min(upper, h.heights[i] + h.heights[1-i]);
						}
					}
				}
				q.set(i, hypernyms);
			}
			height++;
		}

		return height > MAX_HEIGHT ? DISTANCE_NOT_FOUND : upper;
	}

	private List<Synset> getSynsets(String w0, Set<String> g0) {
		Synset[] synsets = database.getSynsets(w0);
		List<Synset> synsetsList = Arrays.asList(synsets);
		return getTopPercentileSynsets(synsetsList, w0, 0.75);
	}

	private List<Synset> getTopPercentileSynsets(List<Synset> synsetsList,
			String w0, double p) {
		int s = 0;
		for (Synset synset : synsetsList) {
			s += getTagCount(synset, w0);
		}
		int bound = (int)Math.ceil(s * p);
		
		int s1 = 0;
		List<Synset> result = new ArrayList<Synset>(bound);
		for (Synset synset : synsetsList) {
			result.add(synset);
			s1 += getTagCount(synset, w0);
			if (s1 >= bound)
				break;
		}
		return result;
	}
	
	private int getTagCount(Synset synset, String w0) {
		String lemma = getLemma(synset, w0);
		return synset.getTagCount(lemma);
	}

	private String getLemma(Synset synset, String w0) {
		String[] wordForms = synset.getWordForms();	
		String[] baseFormCandidates = Morphology.getInstance().getBaseFormCandidates(w0, synset.getType());
		for (String form : wordForms) {
			if (form.equalsIgnoreCase(w0)) {
				return w0;
			}
			for (String candidate : baseFormCandidates) {
				if (candidate.equalsIgnoreCase(form)) {
					return candidate;
				}
			}
		}
		throw new RuntimeException("WordNet didn't find lemma for " + w0 + " in synset " + synset);
	}


	class WorkItem implements Comparable<WorkItem>{
		Synset synset;
		String target;
		int baseDistance;
		WorkItem prev;
		boolean wasNarrowed;
		
		public WorkItem(Synset synset, String target, int baseDist, WorkItem prev, boolean wasNarrowed) {
			this.synset = synset;
			this.target = target;
			this.baseDistance = baseDist;
			this.prev = prev;
			this.wasNarrowed = wasNarrowed;
		}

		public int compareTo(WorkItem other) {
			return this.baseDistance - other.baseDistance;
		}
	}

	public static Map<String, Double> reduceWordsWithSameBaseFrom(Map<String, Double> g) {
		Map<String, Double> result = new TreeMap<String, Double>();
		
		// baseForms -> words
		Map<Set<String>, List<String>> baseForms = new HashMap<Set<String>, List<String>>(); 
		
		for (String w : g.keySet()) {
			String[] baseFormCandidates = Morphology.getInstance().getBaseFormCandidates(w, SynsetType.NOUN);
			//length is mostly 0 or 1, sometimes 2 
			if (baseFormCandidates.length == 0) {
//				System.out.println("NO BASE FOR " + w);
				result.put(w, g.get(w));
			}
			else {
				//System.out.println("baseFormCandidates.length="+baseFormCandidates.length);
				HashSet<String> baseFormsSet = new HashSet<String>(Arrays.asList(baseFormCandidates));
				List<String> wList =baseForms.get(baseFormsSet);
				if (wList == null) {
					// new forms
					wList = new ArrayList<String>();
					wList.add(w);
					baseForms.put(baseFormsSet, wList);
					
					result.put(w, g.get(w));
				}
				else {
					wList.add(w);
					
					double sw = 0.0;
					for (String w1 : wList) {
						sw += g.get(w1); 
					}
					double aw = sw / wList.size();

					String representative = wList.get(0);
					result.put(representative, aw);
				}
			}
		}
//		System.out.println("reduction="+(g.size() - result.size()));
		// reduction is almost always 0 
		return result;
	}

	
	// TODO consider stems, morphology etc.
	private boolean equivalentWords(String w1, String w2) {
		return w1.equalsIgnoreCase(w2);
	}
	

	
	@SuppressWarnings("unused")
	private void printItemsTrace(WorkItem item) {
		if (item.prev != null) {
			printItemsTrace(item.prev);
		}
		System.out.println(item.baseDistance + ": " +  item.synset);
	}

	public static boolean isWordNetWord(String s) {
		boolean isWord = database.getSynsets(s).length > 0;
		return isWord;
	}
	
	public static void main(String[] args) {
		System.out.println(isWordNetWord("category"));
		System.out.println(isWordNetWord("category's"));
		System.out.println(isWordNetWord("student"));
		System.out.println(isWordNetWord("student's"));
		System.out.println(isWordNetWord("students"));
		System.out.println(isWordNetWord("aren't"));
		System.out.println(isWordNetWord("arent"));
		System.out.println(isWordNetWord("are"));
		System.out.println(isWordNetWord("are not"));

	}
}

//@Aspect
//class Profile {
//	@Before("call(* WordNetDatabase.getSynsets(String))")
//	public void doBefore() {
//		System.out.println("before: ");// + thisJoinPointStaticPart.getSignature());
//	}	
//}